package components;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

//TODO: Break up this class? Make UDF a separate class and give an abstract super-classs?
//TODO: Check to make sure all synchronization and thread-safe methods and variables are necessary, remove those that aren't.
/**
 * The synapse class. As of now there is no super-class. Synapses by default
 * use UDF (See Maass, 2002 for details) short term plasticity, but can
 * function similarly to connectionist weights as an option.
 * @author zach
 *
 */
public class Synapse {

	/** The default initial value of R used in UDF. */
	public static final double DEFAULT_INITIAL_R = 1.0;
	
	/** The last time a spike (action potential) was sent down this synapse. */
	private double spikeTime = 0;
	
	/** The current time in the simulation*/
	private double time = 0;
	
	/** 
	 * The delay (i.e. the amount of time a spike takes to react the target
	 * neuron after the source fires) of this synapse
	 */
	private double delay;
	
	/** 
	 * The decay constant. That is, over time, this is the constant by which
	 * current arriving over this synapse at a target neuron decays.
	 */
	private double decay; 
	
	/** The absolute efficacy or "weight" of this synapse. */
	private double w;
	
	/** 
	 * The post-synaptic response, or the total current transferred after
	 * all operations have been applied.
	 */
	private double PSR;
	
	//TODO: Cite original UDF paper... even if Maass 2002 provides better implementation
	/** 
	 * The U parameter of UDF. Initial value for the variable u in UDF which
	 * represents the effect of paired-pulse facilitation on synaptic efficacy.
	 * (see: Maass, 2002). 
	 */
	private double U;
	
	/** 
	 * The D parameter of UDF. paired-pulse depression time constant
	 * (see: Maass, 2002).
	 */
	private double D;
	
	/** The F parameter of UDF, paired-pulse facilitation time constant
	 * (see: Maass, 2002).
	 */
	private double F;
	
	/** 
	 * Initial value for variable r in UDF which represents the effect of
	 * paired-pulse depression on the efficacy of the synapse.
	 */
	private double R = DEFAULT_INITIAL_R;
	
	/** 
	 * The u variable in UDF which represnts the effect of paired-pulse 
	 * facilitation on synaptic efficacy. 
	 */
	private double u;
	
	/** 
	 * Determines whether or not UDF is used to determine synaptic
	 * efficacy.
	 */
	private boolean UDFon = true;
	
	/** A blocking queue of the current of the action potentials have been sent
	 * down this synapse. Due to the delay value it is technically possible 
	 * that the source neuron could spike before the last spike finished 
	 * traversing the synapse. This queue keeps track of those times.
	 */
	private BlockingQueue<Double> spikes =
			new LinkedBlockingQueue<Double>();
	
	/** A blocking queue of the times that action potentials have been sent
	 * down this synapse. Due to the delay value it is technically possible 
	 * that the source neuron could spike before the last spike finished 
	 * traversing the synapse. This queue keeps track of those times.
	 */
	private BlockingQueue<Double> spikeTimes =
			new LinkedBlockingQueue<Double>();
	
	/** A reference to the source neuron of this synapse. */
	private AbstractNeuron src;
	
	/** A reference to the target neuron of this synapse. */
	private AbstractNeuron targ;
	
	public static long nans = 0L;
	
	public static long occurances = 0L;
	
	public static long length = 0L;
	
	public static long invocations = 0L;
	
	private double A;
	
	/**
	 * A bare-bones constructor, usually awaiting a randomizer.
	 * @param src
	 * @param targ
	 */
    public Synapse(AbstractNeuron src, AbstractNeuron targ) {
        this.src = src;
        this.targ = targ;
    }
    
    /**
     * 
     * @param src
     * @param targ
     * @param delay
     * @param decay
     * @param w
     */
    public Synapse(AbstractNeuron src, AbstractNeuron targ, double delay,
			double decay, double w) {
		this(src, targ);
		this.delay = delay;
		this.decay = decay;
        this.w = w;
	}
    
    /**
     * Copy Constructor.
     * @param s
     */
	public Synapse (Synapse s) {
		this.src = s.getSrc();
		this.targ = s.getTarg();
		this.delay = s.getDelay();
		this.decay = s.getDecay();
		this.spikeTime = s.getSpikeTime();
		this.time = s.getTime();
		this.w = s.getStrength();
		this.u = s.getU();
		this.F = s.getF();
		this.D = s.getD();
		this.U = s.getBigU();
		this.R = s.getR();
		this.UDFon = s.isUDFon();
		this.PSR = s.getPSR();
		this.spikeTimes = new LinkedBlockingQueue<Double>();
		for(Double t : s.getSpikeTimes()){
			this.spikeTimes.add(t.doubleValue());
		}
		this.spikes = new LinkedBlockingQueue<Double>();
		for(Double t : s.getSpikes()) {
			this.spikes.add(t.doubleValue());
		}
	}
	
	/**
	 * Insert any synapse plasticity rules here. Called for a neuron's efferent
	 * synapses when it spikes. UDF is calculated here.
	 * @param time time of charge (src has spiked).
	 */
	public void charge(double time) {
		invocations++;
		double ISI = time - spikeTime;
		if(ISI < 0) {
			return;
		}

		
		if(UDFon) {
			A = w * u * R;
			u = u * Math.exp(-(ISI/(F))) + U * (1 - u * Math.exp(-(ISI/(F))));
			R = R * (1 - u) * Math.exp(-(ISI/(D))) + 1 - Math.exp(-(ISI/(D)));
		} else {
			A = w;
		}
		synchronized (this) {
			spikeTime = time + delay;
			try {
				if(A == Double.NaN) {
					nans++;
					A = 0;
					u = U;
					R = 1;
				} else {
					spikes.put(A);
					spikeTimes.put(time + delay);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		length += spikes.size();
	}
	
	/**
	 * Updates the synapse. Called on all afferent synapses to a neuron at
	 * every timestep. Here the post-synaptic response is calculated based 
	 * on the absolute efficacy and the result of UDF as well as the decay
	 * constant. The current time for this synapse is also updated.
	 * @param timeStep the amount of time over which integration is occuring in
	 * the simulation.
	 */
	public synchronized void update(double timeStep) {
		time = time + timeStep;
		PSR = PSR - (PSR * timeStep/decay);
		double act = 0.0;
		if(!spikeTimes.isEmpty()){
			try {
				while (!spikes.isEmpty() && time >= spikeTimes.peek()) {
					if(src.getFanIn() != null) {
						occurances++;
					}
					act += spikes.take();
					spikeTimes.take();			
				}
							
		
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		PSR += act;
	}
	
	/** Returns the post-synaptic response (the final current value being
	 * passed on) as a negative value if the source is inhibitory and positive
	 * otherwise.
	 * 
	 * @return the post-synaptic response of the synapse at this time 
	 */
	public double getPSR() {
		if(src.isExcitatory())
			return PSR;
		else
			return -PSR;
	}
	
	/** Creates a deep-copy of this synapse. */
	public Synapse deepCopy(){
		return new Synapse(this);
	}
	
	
	
	/*/********************************************************************
	 * 						GETTERS AND SETTERS 						  *
	 **********************************************************************/
	
	public AbstractNeuron getSrc() {
		return src;
	}


	public void setSrc(AbstractNeuron src) {
		this.src = src;
	}


	public AbstractNeuron getTarg() {
		return targ;
	}

	public void setTarg(AbstractNeuron targ) {
		this.targ = targ;
	}
	
	public double getSpikeTime() {
		return spikeTime;
	}

	public void setSpikeTime(double spikeTime) {
		this.spikeTime = spikeTime;
	}

	public double getDelay() {
		return delay;
	}

	public void setDelay(double delay) {
		this.delay = delay;
	}

	public double getStrength() {
		return w;
	}

	public void setStrength(double strength) {
		this.w = strength;
	}

	public double getDecay() {
		return decay;
	}

	public void setDecay(double decay) {
		this.decay = decay;
	}

	public double getW() {
		return w;
	}

	public void setW(double w) {
		this.w = w;
	}

	public double getU() {
		return u;
	}

	public void setU(double u) {
		U = u;
		this.u = u;
	}

	public double getD() {
		return D;
	}

	public void setD(double d) {
		D = d;
	}

	public double getF() {
		return F;
	}

	public void setF(double f) {
		F = f;
	}

	public double getR() {
		return R;
	}

	public void setR(double r) {
		R = r;
	}


	public void setPSR(double pSR) {
		PSR = pSR;
	}

	public boolean isUDFon() {
		return UDFon;
	}

	public void setUDFon(boolean uDFon) {
		UDFon = uDFon;
	}
	
	public void setTime(double time) {
		this.time = time;
	}
	
	public double getBigU(){
		return U;
	}
	
	public BlockingQueue<Double> getSpikeTimes(){
		return spikeTimes;
	}
	
	public BlockingQueue<Double> getSpikes(){
		return spikes;
	}
	
	public double getTime(){
		return time;
	}
	
	public void clearSpkQueues(){
		spikeTimes.clear();
		spikes.clear();
		spikeTime = 0;
		PSR = 0;
	}

	public double getA() {
		return A;
	}

	public void setA(double a) {
		A = a;
	}
	
}
